/*
 * Famon (http://code.google.com/p/famon/)
 * Copyright (c) 2010 Carl Crous.
 *
 * This file is part of Famon.
 *
 * Famon is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Famon is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Famon.  If not, see <http://www.gnu.org/licenses/>.
 */

package za.org.foobar.famon.util;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.datanucleus.jdo.JDOQuery;
import za.org.foobar.famon.model.ModelObject;
import za.org.foobar.famon.webapp.FamonException;

import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.jdo.Transaction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;


/**
 * Manages a static EntityManagerFactory for our application.
 * Associates an EntityManager with each Thread using a ThreadLocal and
 * provides static methods that operate using this EM.
 */
public class DB {

    private static ThreadLocal DB_TL = new ThreadLocal();
    private static long debugTime;

    private PersistenceManager pm;
    private boolean forRollbackOnly;

    private static final Log log = LogFactory.getLog(DB.class);

    private DB() {
    }

    private static DB get() {
        DB db = (DB)DB_TL.get();
        if (db == null) {
            DB_TL.set(db = new DB());
        }
        return db;
    }

    /**
     * Get the PersistenceManager associated with the calling thread. An EM is
     * created if needed. If a new EM is created then a transaction is
     * started.
     */
    public static PersistenceManager getPM() {
        DB db = get();
        if (db.pm == null) {
            db.pm = getNewPM(db);
        }
        return db.pm;
    }

    private static synchronized PersistenceManager getNewPM(DB db) {
        if (db.pm == null) {
            return PersistenceManagerFactoryBean.get().getPersistenceManager();
        }
        return db.pm;
    }

    /**
     * Close the EM associated with the calling thread (if any). If a
     * transaction is active it is rolled back.
     */
    public static void closePM() {
        DB db = get();
        PersistenceManager pm = db.pm;
        if (pm != null) {
            db.forRollbackOnly = false;
            if (!pm.isClosed()) {
                Transaction tx = pm.currentTransaction();
                if (tx.isActive()) {
                    log.error("ClosePM Called when there was an active transaction! This was rolled back.");
                    tx.rollback();
                }
                if (log.isDebugEnabled()) {
                    debugTime = System.currentTimeMillis();
                    pm.close();
                    debugTime = System.currentTimeMillis() - debugTime;
                    log.debug("ClosePM took " + debugTime + " ms");
                } else {
                    pm.close();
                }
            }
            db.pm = null;
        }
    }

    /**
     * Begin a transaction if one has not already been started.
     */
    public static void begin() {
        PersistenceManager em = getPM();
        if (log.isDebugEnabled()) {
            log.debug("DB.begin()");
            debugTime = System.currentTimeMillis();
        }
        if (em.currentTransaction().isActive()) {
            throw new IllegalStateException("DB.begin() called when a transaction is already active");
        }
        em.currentTransaction().begin();
    }

    /**
     * Commit the current transaction if one is active.
     */
    public static void commit() {
        if (isForRollbackOnly()) {
            throw new IllegalStateException(
                    "Transaction has been flagged for rollback only");
        }
        Transaction tx = getPM().currentTransaction();
        if (!tx.isActive()) {
            throw new IllegalStateException("DB.commit() called when no transaction is active");
        }
        tx.commit();
        if (log.isDebugEnabled()) {
            debugTime = System.currentTimeMillis() - debugTime;
            log.debug("DB.commit() transaction took " + debugTime + "ms from begin");
        }
    }

    /**
     * Rollback the current transaction if one is active.
     * Throw an exception if there isn't one.
     */
    public static void rollback() {
        PersistenceManager em = getPM();
        Transaction tx = getPM().currentTransaction();
        if (!tx.isActive()) {
            throw new IllegalStateException("DB.rollback() called when no transaction is active");
        }
        em.currentTransaction().rollback();
        if (log.isDebugEnabled()) {
            debugTime = System.currentTimeMillis() - debugTime;
            log.debug("DB.rollback() transaction took " + debugTime + "ms from begin");
        }
    }

    /**
     * Rollback the current traction if one is active.
     */
    public static void rollbackOpenTransaction() {
        PersistenceManager em = getPM();
        Transaction tx = getPM().currentTransaction();
        if (tx.isActive()) {
            em.currentTransaction().rollback();
            if (log.isDebugEnabled()) {
                debugTime = System.currentTimeMillis() - debugTime;
                log.debug("DB.rollbackOpenTransaction() transaction took " + debugTime + "ms from begin");
            } else {
                log.debug("DB.rollbackOpenTransaction()");
            }
        }
    }

    /**
     * Flag the current transaction for rollback. Any commit call will result
     * in an IllegalStateException.
     */
//    public static void setForRollbackOnly() {
//        get().forRollbackOnly = true;
//    }

    /**
     * Has the current transaction been marked for rollback?
     */
    public static boolean isForRollbackOnly() {
        return get().forRollbackOnly;
    }

    /**
     * Flush all dirty objects to the database.
     */
    public static void flush() {
        if (log.isDebugEnabled()) {
            debugTime = System.currentTimeMillis();
            getPM().flush();
            debugTime = System.currentTimeMillis() - debugTime;
            log.debug("DB.flush() took " + debugTime + "ms to flush");
        } else {
            getPM().flush();
        }
    }

    public static void persist(Object o) {
        PersistenceManager em = getPM();
//        if (!em.currentTransaction().isActive()) {
//            throw new IllegalStateException("DB.persist() called when no transaction is active");
//        }
        em.makePersistent(o);
    }

    public static void persistAll(Object[] a) {
        PersistenceManager em = getPM();
        em.makePersistentAll(a);
    }

    public static <T> void persistAll(Collection<T> c) {
        PersistenceManager em = getPM();
        em.makePersistentAll(c);
    }

    public static <T extends ModelObject> T merge(T o) {
        PersistenceManager em = getPM();
//        if (!em.currentTransaction().isActive()) {
//            throw new IllegalStateException("DB.persist() called when no transaction is active");
//        }
        return (T)em.getObjectById(o.getClass(), o.getId());
    }

    public static void remove(Object o) {
        PersistenceManager em = getPM();
//        if (!em.currentTransaction().isActive()) {
//            throw new IllegalStateException("DB.persist() called when no transaction is active");
//        }
        em.deletePersistent(o);
    }

    /**
     * Find the entity with id or return null if it does not exist.
     */
    public static <T> T find(Class<T> cls, Object id) {
        try {
            return getPM().getObjectById(cls, id);
        } catch (JDOObjectNotFoundException e) {
            return null;
        }
    }

    /**
     * Find the entity with id or thrown an exception if it does not exist.
     */
    public static <T> T findEx(Class<T> cls, Object id) {
        T ans = find(cls, id);
        if (ans == null) {
            throw new FamonException(cls.getSimpleName() + " not found for ID " +
                    id);
        }
        return ans;
    }

    /**
     * Find the entity with name or return null if it does not exist.
     */
    public static <T> T findByName(Class<T> cls, String name) {
        List<T> list = createDBQuery(cls)
                .addWhere("name == nameParam")
                .addParameter("nameParam", name)
                .getResultList();
        if (list.size() > 1) {
            for (T o : list) {
                log.info(Thread.currentThread().getName() + ": Returned \"" + o + "\"");
            }
            throw new FamonException("DB.findByName used on a table duplicates for name: " + name +
                    ". Found " + list.size() + " matches.");
        }
        return list.isEmpty() ? null : (T)list.get(0);
    }

    public static Query createQuery(String jdoql) {
        return getPM().newQuery(jdoql);
    }

    public static Query createQuery(Class cls) {
        return getPM().newQuery(cls);
    }

    public static <T> DBQuery<T> createDBQuery(Class<T> cls) {
        return new DBQuery<T>((JDOQuery)getPM().newQuery(cls));
    }

    public static <T> DBQuery<T> createDBQuery(Class<T> cls, String fields) {
        return new DBQuery<T>((JDOQuery)getPM().newQuery("SELECT " + fields + " FROM " + cls.getName()));
    }

    public static List<String> getKindList(boolean includeSystemKinds) {
        List<String> kinds = new ArrayList<String>();
        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
        List<Entity> stats = datastore.prepare(new com.google.appengine.api.datastore.Query("__Stat_Kind__"))
                .asList(FetchOptions.Builder.withLimit(1000));
        for (Entity e : stats) {
            String kindName = (String)e.getProperty("kind_name");
            if (kindName != null && (includeSystemKinds || !kindName.startsWith("__"))) {
                kinds.add(kindName);
            }
        }
        return kinds;
    }

}
